﻿//-----------------------------------------------------------------------
// <copyright file="ExceptionExtensions.cs" company="CodePlex">
//     Copyright (c) Doug Holland, CodePlex. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace CodePlex.Diagnostics
{
    using System;
    
    using System.Collections;
    using System.Collections.Generic;

    using System.Diagnostics;

    using System.Reflection;

    using System.Security;
    using System.Security.Principal;

    using System.ServiceModel;

    using System.Threading;
    
    using System.Xml;
    using System.Xml.Linq;
    
    using CodePlex.Diagnostics.Contracts;

    /// <summary>
    /// Provides extension methods for the <see cref="T:System.Exception"/> class to enable the publishing of exceptions using the CodePlex.Diagnostics framework.
    /// </summary>
    public static class ExceptionExtensions
    {
        #region public static Guid Publish(this Exception ex)

        /// <summary>
        /// Publishes the <see cref="T:System.Exception"/> using the configured exception provider.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <returns>A <see cref="T:System.Guid"/> representing the unique publication identifier for an exception and associated inner exceptions.</returns>
        [DebuggerStepThrough]
        public static Guid Publish(this Exception ex)
        {
            // given that an identity has not been specified the current Windows identity will be used to publish the exception.
            IIdentity identity = WindowsIdentity.GetCurrent() as IIdentity;

            return Publish(ex, identity);
        }

        #endregion

        #region public static Guid Publish(this Exception ex, IIdentity identity)

        /// <summary>
        /// Publishes the <see cref="T:System.Exception"/> using the configured exception provider.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <param name="identity">The <see cref="T:System.Security.Principal.IIdentity"/> to associate with this published exception.</param>
        /// <returns>A <see cref="T:System.Guid"/> representing the unique publication identifier for an exception and associated inner exceptions.</returns>
        [DebuggerStepThrough]
        public static Guid Publish(this Exception ex, IIdentity identity)
        {          
           return Publish(ex, null, identity); 
        }

        #endregion
        
        #region public static Guid Publish(this Exception ex, Dictionary<string, string> dictionary, IIdentity identity)

        /// <summary>
        /// Publishes the <see cref="T:System.Exception"/> using the configured exception provider.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <param name="dictionary">A <see cref="T:System.Collections.Generic.Dictionary`2"/> containing name value pairs associated with this exception.</param>
        /// <param name="identity">The <see cref="T:System.Security.Principal.IIdentity"/> to associate with this published exception.</param>
        /// <returns>A <see cref="T:System.Guid"/> representing the unique publication identifier for an exception and associated inner exceptions.</returns>
        [DebuggerStepThrough]
        public static Guid Publish(this Exception ex, Dictionary<string, string> dictionary, IIdentity identity)
        {
            return Publish(ex, dictionary, null, identity);
        }
        
        #endregion

        #region public static Guid Publish(this Exception ex, Dictionary<string, string> dictionary, XElement element, IIdentity identity)

        /// <summary>
        /// Publishes the <see cref="T:System.Exception"/> using the configured exception provider.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <param name="dictionary">A <see cref="T:System.Collections.Generic.Dictionary`2"/> containing name value pairs associated with this exception.</param>
        /// <param name="element">A <see cref="T:System.Xml.Linq.XElement"/> representing an xml element to be associated with this exception.</param>
        /// <param name="identity">The <see cref="T:System.Security.Principal.IIdentity"/> to associate with this published exception.</param>
        /// <returns>A <see cref="T:System.Guid"/> representing the unique publication identifier for an exception and associated inner exceptions.</returns>
        [DebuggerStepThrough]
        public static Guid Publish(this Exception ex, Dictionary<string, string> dictionary, XElement element, IIdentity identity)
        {
            // establish a new publication identifier for the exception.
            Guid publicationId = Guid.NewGuid();
            
            // determine if the exception has already been published.
            IPublishedException publishedException = ex as IPublishedException;

            if (publishedException != null)
            {
                // exception has already been published, return the publication identifier.
                return publishedException.PublicationId;
            }

            // establish an exception data contract based upon the supplied exception object.
            ExceptionDataContract exceptionDataContract = new ExceptionDataContract(ex, publicationId);

            // establish an identity data contract based upon the supplied identity object.
            IIdentityDataContract identityDataContract = new IIdentityDataContract(identity);

            // establish the application domain data contract based upon the current domain.
            exceptionDataContract.AppDomain = new AppDomainDataContract(AppDomain.CurrentDomain);

            StackTrace stackTrace = new StackTrace();

            Assembly assembly = null;

            for (int index = stackTrace.FrameCount - 1; index >= 0; index--)
            {
                StackFrame frame = stackTrace.GetFrame(index);

                MethodBase method = frame.GetMethod();

                if (method.DeclaringType.Assembly != Assembly.GetAssembly(typeof(ExceptionExtensions)))
                {
                    assembly = method.DeclaringType.Assembly;

                    break;
                }
            }

            // establish the assembly data contract based upon the calling assembly, that is the assembly containing the method calling this method.
            exceptionDataContract.Assembly = new AssemblyDataContract(assembly);

            // establish the machine data contract.
            exceptionDataContract.Machine = new MachineDataContract();

            // establish the process data contract based upon the current process.
            exceptionDataContract.Process = new ProcessDataContract(Process.GetCurrentProcess());

            // establish the thread data contract based upon the current thread.
            exceptionDataContract.Thread = new ThreadDataContract(Thread.CurrentThread);

            DiagnosticsServiceClient client = null;

            try
            {
                // establish a proxy to the diagnostics service.
                client = new DiagnosticsServiceClient();

                // publish the exception using the diagnostics service, returning the publication identifier.
                IAsyncResult result = client.BeginPublishException(exceptionDataContract, dictionary, element, identityDataContract, null, null);

                // complete the publication of the exception and establish the publication Id.
                client.EndPublishException(result);

                // causes the client to transition from its current state into the closed state.
                client.Close();

                return publicationId;
            }
            catch
            {
                // causes the client to transition immediately from its current state into the closed state.
                if (client != null)
                {
                    client.Abort();
                }

                return Guid.Empty;
            }
        }

        #endregion        
    }
}